﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Timers;
using System.IO;
using System.Reflection;

namespace Trogsoft.IRC {

    /// <summary>
    /// Trogsoft IRC Client Class
    /// </summary>
    public class IRCClient {

        private string ircServer { get; set; }
        private int ircPort { get; set; }   
        private string nick { get; set; }
        private string alternativeNick { get; set; }
        private List<string> channels = new List<string>();
        private TcpClient connection;
        private Thread dataChecker;
        private bool isJoined = false;
        private string joinedServerName = null;
        private System.Timers.Timer pingTimer = new System.Timers.Timer(1000);
        private int pingCounter = 0;

        /// <summary>
        /// Get or set a boolean value which specifies wheether the IRC Client should reconnect if disconnected.
        /// </summary>
        public bool AutoReconnect { get; set; }

        /// <summary>
        /// Indicates whether the client is connected to IRC or not.
        /// </summary>
        public bool IsConnected { get; private set; }

        /// <summary>
        /// Sets whether the IRC Client will throw exceptions for non-fatal errors
        /// </summary>
        public bool ThrowMildExceptions { get; set; }

        /// <summary>
        /// Read only list of channels the client is configured to join.
        /// </summary>
        public List<string> Channels {
            get { return channels; } 
        }

        /// <summary>
        /// Sets or gets the IRC Server.  Cannot be set while connected or an exception will be thrown.
        /// </summary>
        public string IrcServer {
            get { return ircServer; }
            set {
                if (!IsConnected) {
                    ircServer = value;
                } else {
                    if (ThrowMildExceptions) throw new InvalidOperationException("Cannot change server while connected to IRC.");
                }
            }
        }

        /// <summary>
        /// Sets or gets the port used for connecting to IRC.  Cannot be set while connected.
        /// </summary>
        public int IrcPort {
            get { return ircPort; }
            set {
                if (!IsConnected) {
                    ircPort = value;
                } else {
                    if (ThrowMildExceptions) throw new InvalidOperationException("Cannot change port while connected to IRC.");
                }
            }
        }

        /// <summary>
        /// Gets or sets the nickname to be used on IRC.  If connected, the currently used nick will be changed.
        /// </summary>
        public string IrcNick {
            get { return nick; }
            set {
                if (!IsConnected) {
                    nick = value;
                } else {
                    nick = value;
                    SendRaw("NICK " + value);
                }
            }
        }

        /// <summary>
        ///  Gets or sets the alternate nickname used when the nickname is in use.
        /// </summary>
        public string IrcAlternateNick {
            get { return alternativeNick; }
            set { alternativeNick = value; }
        }

        /// <summary>
        ///  Adds a channel to the list of channels to join when connected to IRC.
        /// </summary>
        /// <param name="channel">Channel name to join.</param>
        public void AddChannelToJoin(string channel) {
            channels.Add((channel).Trim());
        }

        /// <summary>
        ///  Removes a channel from the list of channels to join.
        /// </summary>
        /// <param name="channel">Channel name</param>
        public void RemoveChannel(string channel) {
            if (channels.Contains(channel.Trim()))
            {
                try
                {
                    channels.Remove(channel.Trim());
                }
                catch
                {
                    throw new InvalidOperationException("Cannot remove channel from channel list.");
                }
            }
            else
            {
                if (ThrowMildExceptions) throw new ArgumentException("Channel does not exist in channel list.");
            }
        }

        /// <summary>
        /// Create an instance of the IRCClient class.
        /// </summary>
        /// <param name="server">The IRC server to connect to.</param>
        /// <param name="port">The port number on which to connect to the IRC server.</param>
        public IRCClient(string server, int port) {
            ircServer = server;
            ircPort = port;
            pingTimer.Elapsed += new ElapsedEventHandler(PingTimer_Elapsed);
        }

        /// <summary>
        /// Create an instance of the IRCClient class.
        /// </summary>
        /// <param name="server">The IRC server to connect to.</param>
        /// <param name="port">The port number on which to connect to the IRC server.</param>
        /// <param name="nick">The nickname to use</param>
        /// <param name="altnick">The alternative nickname to use</param>
        public IRCClient(string server, int port, string nick, string altnick) {
            ircServer = server;
            ircPort = port;
            this.nick = nick;
            alternativeNick = altnick;
            pingTimer.Elapsed += new ElapsedEventHandler(PingTimer_Elapsed);
        }

        /// <summary>
        /// Create an instance of the IRCClient class.
        /// </summary>
        public IRCClient() {
            pingTimer.Elapsed += new ElapsedEventHandler(PingTimer_Elapsed);
        }

        /// <summary>
        ///  Connect to IRC.
        /// </summary>
        public void Connect() {

            if (ircServer != "" && ircPort > 0 && nick != "") {

                while (true) {
                    connection = new TcpClient(ircServer, ircPort);
                    break;
                }

                try {
                    SendRaw("NICK " + this.nick);
                    SendRaw("USER " + this.nick + " test test test");
                } catch {
                    throw new Exception("Unable to register with IRC.");
                }

                dataChecker = new Thread(this.ReadData);
                dataChecker.Start();
                pingTimer.Enabled = true;
                this.IsConnected = true;
                if (OnConnect != null) { OnConnect(this); }

            } else {
                throw new InvalidOperationException("No server or port specified.");
            }                   

        }

        /// <summary>
        /// Increment the ping counter.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void PingTimer_Elapsed(object sender, ElapsedEventArgs e) {

            pingCounter++;
            if (pingCounter > 240) {
                pingTimer.Enabled = false;
                pingCounter = 0;
                Disconnect();
            }

        }

        /// <summary>
        ///  Disconnect from IRC.
        /// </summary>
        public void Disconnect() {

            this.IsConnected = false;
            isJoined = false;
            dataChecker.Abort();
            pingCounter = 0;

            try {
                connection.Client.Disconnect(true);
            } catch {
            }

            if (OnDisconnect != null) { OnDisconnect(this); }

            if (AutoReconnect) {
                Connect();
            }

        }

        /// <summary>
        ///  Send raw data to the IRC Connection
        /// </summary>
        /// <param name="data">Raw data to send to IRC.</param>
        public void SendRaw(string data) {

            ASCIIEncoding ae = new ASCIIEncoding();
            byte[] d;
            d = ae.GetBytes(data + "\r\n");

            try {
                NetworkStream ns = connection.GetStream();
                ns.Write(d, 0, d.Length);
            } catch {
            }

        }

        /// <summary>
        ///  Send a private message to an IRC user or channel.
        /// </summary>
        /// <param name="Target">IRC User or channel</param>
        /// <param name="Message">The message you wish to send.</param>
        public void Say(string Target, string Message) {
            SendRaw("PRIVMSG " + Target + " :" + Message);
        }

        /// <summary>
        ///  Reads data from the IRC connection.
        /// </summary>
        public void ReadData() {

            NetworkStream ns = connection.GetStream();
            StringBuilder line = new StringBuilder();

            do {

                try {
                    if (ns.DataAvailable) { }
                } catch {
                }

                try {
                    if (ns.DataAvailable) {

                        int letter = -1;
                        try {
                            letter = ns.ReadByte();
                        } catch {
                        }

                        if (letter > -1) {

                            if (letter == 10) { continue; }

                            /// Once we receive a carriage return we will process the line
                            if (letter == 13) {

                                string ll = line.ToString();
                                line.Remove(0, line.Length);

                                // Send the line to any raw data handlers.
                                if (OnRawData != null) { OnRawData(this, new IrcEvent(String.Empty, String.Empty, IrcEventType.RawData, ll, String.Empty)); }

                                // Reset the ping counter now we've received something
                                pingCounter = 0;

                                // Deal with ping requests from the server
                                if (ll.ToString().StartsWith("PING :")) {
                                    if (OnPing != null) { OnPing(this); }
                                    string rid = ll.ToString().Substring(6, ll.Length - 6);
                                    SendRaw("PONG :" + rid);
                                    if (!isJoined) {
                                        foreach (string c in channels) {
                                            SendRaw("JOIN " + c);
                                        }
                                        if (OnBeforeJoinChannels != null && !isJoined) { OnBeforeJoinChannels(this); }
                                        isJoined = true;
                                    }
                                }

                                // Record the name of the server we've joined.
                                if (ll.ToString().StartsWith(":")) {

                                    if (joinedServerName == null) {
                                        joinedServerName = ll.ToString().Substring(1, ll.ToString().IndexOf(' '));
                                    }

                                }

                                // If we have a message coming from the server...
                                if (ll.ToString().StartsWith(":" + joinedServerName)) {

                                    // ... this is a Server Status Message
                                    string l = ll.ToString().Substring(ll.ToString().IndexOf(' ')).Trim();
                                    IrcEvent i = new IrcEvent();
                                    i.EventData = l.Substring(l.IndexOf(' ')).Trim();
                                    i.StatusId = l.Trim().Substring(0, l.IndexOf(' '));

                                    // Send it to the event handler
                                    if (OnStatusMessage != null) {
                                        OnStatusMessage(this, i);
                                    }

                                } else if (ll.ToString().StartsWith(":")) {

                                    // Other types of message

                                    // Split the message into bits at the spaces
                                    string[] bits = ll.ToString().TrimStart(' ', ':').Split(new char[] { ' ' }, 4);
                                    if (bits.Length == 3) {

                                        string nickandhost = bits[0].TrimStart(':');
                                        string nick = nickandhost.Substring(0, nickandhost.IndexOf('!'));
                                        string host = nickandhost.Substring(nickandhost.IndexOf('!') + 1);
                                        string mtype = bits[1];
                                        string dest = bits[2];

                                        // Join message
                                        if (bits[1] == "JOIN") {
                                            if (OnJoinChannel != null) {

                                                if (nick.ToLower() == nick.ToLower() || nick.ToLower() == alternativeNick.ToLower()) {
                                                    // The IRC client joined a channel
                                                    OnJoinChannel(this, new IrcEvent(nick, dest, IrcEventType.BotJoinChannel, "", ""));
                                                } else {
                                                    // Someone else joined a channel
                                                    OnJoinChannel(this, new IrcEvent(nick, dest, IrcEventType.JoinChannel, "", ""));
                                                }

                                            }
                                        }

                                        if (bits[1] == "NICK") {
                                            // Someone changed their nick
                                            if (OnNickChange != null) {
                                                OnNickChange(this, new IrcEvent(nick, dest, IrcEventType.NickChange, String.Empty, dest.TrimStart(':')));
                                            }
                                        }

                                        if (bits[1] == "PART") {
                                            // Someone left a channel
                                            if (OnPartChannel != null) {
                                                OnPartChannel(this, new IrcEvent(nick, dest, IrcEventType.PartChannel, String.Empty, String.Empty));
                                            }
                                        }

                                        if (bits[1] == "QUIT") {
                                            // Someone quit IRC
                                            if (OnQuit != null) {
                                                OnQuit(this, new IrcEvent(nick, dest, IrcEventType.Quit, String.Empty, String.Empty));
                                            }
                                        }

                                    }

                                    if (bits.Length == 4) {

                                        // Parse messages with four parts
                                        string nickandhost = bits[0].TrimStart(':');
                                        string nick = "";
                                        string host = "";
                                        string mtype = "";
                                        string dest = "";
                                        string message = "";
                                        if (nickandhost.IndexOf('!') > -1) {
                                            nick = nickandhost.Substring(0, nickandhost.IndexOf('!'));
                                            host = nickandhost.Substring(nickandhost.IndexOf('!')).Trim('!');
                                        }
                                        if (bits.Length == 4) {
                                            mtype = bits[1];
                                            dest = bits[2];
                                            message = bits[3].TrimStart(':');
                                        }

                                        if (bits[1] == "PRIVMSG") {
                                            // Channel message, or private message
                                            if (OnPrivateMessage != null) {
                                                OnPrivateMessage(this, new IrcEvent(nick, dest, IrcEventType.PrivateMessage, message, String.Empty));
                                            }
                                        }

                                        if (bits[1] == "TOPIC") {
                                            // Someone changed the topic.
                                            if (OnTopicChange != null) {
                                                OnTopicChange(this, new IrcEvent(nick, dest, IrcEventType.TopicChange, message, String.Empty));
                                            }
                                        }

                                        if (bits[1] == "PART") {
                                            // Someone left the channel with a quit message
                                            if (OnPartChannel != null) {
                                                OnPartChannel(this, new IrcEvent(nick, dest, IrcEventType.PartChannel, message, String.Empty));
                                            }
                                        }

                                        if (bits[1] == "QUIT") {
                                            // Someone quit IRC with a quit messaage
                                            if (OnQuit != null) {
                                                OnQuit(this, new IrcEvent(nick, dest, IrcEventType.Quit, message, String.Empty));
                                            }
                                        }

                                        if (bits[1] == "MODE") {
                                            // Someone changed a channel mode
                                            if (OnModeChange != null) {
                                                OnModeChange(this, new IrcEvent(nick, dest, IrcEventType.ModeChange, bits[3].Substring(0, bits[3].IndexOf(' ')), bits[3].Substring(bits[3].IndexOf(' ')).Trim()));
                                            }
                                        }

                                        if (bits[1] == "KICK") {
                                            // Someone was kicked.
                                            if (OnKick != null) {
                                                OnKick(this,new IrcEvent(nick,dest,IrcEventType.Kick,"",message));
                                            }
                                        }

                                    }

                                    if (bits.Length == 5) {

                                        // Parse messages with 5 bits of information
                                        string nickandhost = bits[0].TrimStart(':');
                                        string nick = "";
                                        string host = "";
                                        string mtype = "";
                                        string dest = "";
                                        string message = "";
                                        string target = "";
                                        if (nickandhost.IndexOf('!') > -1) {
                                            nick = nickandhost.Substring(0, nickandhost.IndexOf('!'));
                                            host = nickandhost.Substring(nickandhost.IndexOf('!')).Trim('!');
                                        }
                                        if (bits.Length == 5) {
                                            mtype = bits[1];
                                            dest = bits[2];
                                            target = bits[3].TrimStart(':');
                                            message = bits[4].TrimStart(':');
                                        }
                                        
                                        if (bits[1] == "KICK") {
                                            // Someone was kicked with a kick message
                                            if (OnKick != null) {
                                                OnKick(this,new IrcEvent(nick,dest,IrcEventType.Kick,message,target));
                                            }
                                        }

                                    }

                                }

                                line.Remove(0, line.Length);

                            } else {

                                // Append the character we just received to the string currently being built
                                line.Append(Convert.ToChar(letter));

                            }

                        }

                    } else {
                        // If there's no data waiting on the socket, sleep briefly.
                        System.Threading.Thread.Sleep(10);
                    }
                } catch { }

            } while (true);

        }

        public delegate void ConnectHandler(object sender);
        public delegate void DisconnectHandler(object sender);
        public delegate void PingHandler(object sender);
        public delegate void ErrorHandler(object sender);
        public delegate void JoinChannelHandler(object sender, IrcEvent e);
        public delegate void PartChannelHandler(object sender, IrcEvent e);
        public delegate void ModeChangeHandler(object sender, IrcEvent e);
        public delegate void NickChangeHandler(object sender, IrcEvent e);
        public delegate void TopicChangeHandler(object sender, IrcEvent e);
        public delegate void StatusMessageHandler(object sender, IrcEvent e);
        public delegate void PrivateMessageHandler(object sender, IrcEvent e);
        public delegate void RawDataHandler(object sender, IrcEvent e);
        public delegate void QuitHandler(object sender, IrcEvent e);
        public delegate void KickHandler(object sender, IrcEvent e);
        public delegate void BeforeJoinChannels(object sender);

        /// <summary>
        /// An event handler which fires when the IRC Client connects to IRC.
        /// </summary>
        public event ConnectHandler OnConnect;                  // done
        /// <summary>
        /// An event which fires when the IRC Client disconnects from IRC.
        /// </summary>
        public event DisconnectHandler OnDisconnect;            // done
        /// <summary>
        /// An event which fires when a user joins a channel, or when the IRC Client itself joins a channel.
        /// </summary>
        public event JoinChannelHandler OnJoinChannel;          // done
        /// <summary>
        /// An event which fires when a user leaves a channel, or when the IRC Client itself leaves a channel.
        /// </summary>
        public event PartChannelHandler OnPartChannel;          // done
        /// <summary>
        /// An event which fires when a ping request is received from the IRC Server.
        /// </summary>
        public event PingHandler OnPing;                        // done
        /// <summary>
        /// An event which fires when a channel mode is changed.
        /// </summary>
        public event ModeChangeHandler OnModeChange;            
        /// <summary>
        /// An event which fires when a user changes his or her nick on IRC.
        /// </summary>
        public event NickChangeHandler OnNickChange;            // done
        /// <summary>
        /// An event which fires when a channel topic is changed.
        /// </summary>
        public event TopicChangeHandler OnTopicChange;          // done
        /// <summary>
        /// An event which fires when an IRC status message is received.
        /// </summary>
        public event StatusMessageHandler OnStatusMessage;      // done
        /// <summary>
        /// An event which fires when a private message is received on IRC.
        /// </summary>
        public event PrivateMessageHandler OnPrivateMessage;    // done
        /// <summary>
        /// An event which fires when raw data is received from IRC
        /// </summary>
        public event RawDataHandler OnRawData;                  // done
        /// <summary>
        /// An event which fires when a user quits IRC.
        /// </summary>
        public event QuitHandler OnQuit;                        // done
        /// <summary>
        /// An event which fires when a user or the IRC Client is kicked from a channel.
        /// </summary>
        public event KickHandler OnKick;
        /// <summary>
        /// An event which fires just before channels are joined.
        /// </summary>
        public event BeforeJoinChannels OnBeforeJoinChannels;

    }

    /// <summary>
    /// Represents an event which has occured on IRC.
    /// </summary>
    public enum IrcEventType
    {
        /// <summary>
        /// Represents the IRC client successfully connecting to IRC.
        /// </summary>
        Connect,
        /// <summary>
        /// Represents the IRC Client disconnecting from IRC.
        /// </summary>
        Disconnect,
        /// <summary>
        /// Represents a recoverable error of some kind.
        /// </summary>
        Error,
        /// <summary>
        /// Occurs when a user joins a channel in which the IRC Client is present.
        /// </summary>
        JoinChannel,
        /// <summary>
        /// Occurs when the IRC Client joins a channel.
        /// </summary>
        BotJoinChannel,
        /// <summary>
        /// Occurs when a user leaves a channel.
        /// </summary>
        PartChannel,
        /// <summary>
        /// Occurs when the IRC Client leaves a channel
        /// </summary>
        BotPartChannel,
        /// <summary>
        /// Occurs when a channel mode is changed.
        /// </summary>
        ModeChange,
        /// <summary>
        /// Occurs when a user changes their nick in a channel
        /// </summary>
        NickChange,
        /// <summary>
        /// Occurs when the channel topic is changed.
        /// </summary>
        TopicChange,
        /// <summary>
        /// Occurs when a server status message is received.
        /// </summary>
        StatusMessage,
        /// <summary>
        /// Occurs when a private message (PRIVMSG) is received. 
        /// </summary>
        PrivateMessage,
        /// <summary>
        /// Occurs when raw data is received from IRC.
        /// </summary>
        RawData,
        /// <summary>
        /// Occurs when a user is kicked from a channel
        /// </summary>
        Kick,
        /// <summary>
        /// Occurs when a user quits IRC.
        /// </summary>
        Quit
    }


    /// <summary>
    /// Represents an IRC event which has occurred.  This class will contain all details of the event, and will be passed
    /// to event subscribers as appropriate.
    /// </summary>
    public class IrcEvent
    {
        /// <summary>
        /// Usually the channel in which an event occurred
        /// </summary>
        public string Target;
        /// <summary>
        /// The nick of the IRC user who caused a particular event.
        /// </summary>
        public string Nick;
        /// <summary>
        /// The event type
        /// </summary>
        public IrcEventType EventType;
        /// <summary>
        /// Data about the event.  The content of this value depends on the type of property.  For example, 
        /// a PrivateMessage event would place the message text in this property.
        /// </summary>
        public string EventData;
        /// <summary>
        /// The nick or nicks of users an event was targetted at (for example, mode changes).  This is also used
        /// when a user changed their nick.  This property will contain the new nick.
        /// </summary>
        public string TargetNicks;
        /// <summary>
        /// Used for status messages.  This property contains the status number.
        /// </summary>
        public string StatusId;
        /// <summary>
        /// Create an instance of the IrcEvent.  Users of this API should not need to instantiate this class.
        /// </summary>
        /// <param name="nick"></param>
        /// <param name="target"></param>
        /// <param name="etype"></param>
        /// <param name="data"></param>
        /// <param name="tnicks"></param>
        public IrcEvent(string nick, string target, IrcEventType etype, string data, string tnicks)
        {
            Target = target;
            Nick = nick;
            EventType = etype;
            EventData = data;
            TargetNicks = tnicks;
        }
        /// <summary>
        /// Create an instance of the IrcEvent.  Users of this API should not need to instantiate this class.
        /// </summary>
        public IrcEvent() { }
    }

}
